package jenkins;

import org.apache.commons.io.IOUtils;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;

import static io.netty.util.CharsetUtil.UTF_8;

/**
 * jenkins凭证解密
 * <p>
 * 由于某种原因,我们需要对jenkins存储的凭证进行解密.
 * 我们需要使用三个文件,分别是
 * <p>
 * secrets目录下的master.key
 * secrets目录下的hudson.util.Secret
 * home目录下的credentials.xml,这里存储了密文密码,如下  /root/.jenkins/credentials.xml
 * <com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
 * <scope>GLOBAL</scope>
 * <id>xxx</id>
 * <description>xxxx</description>
 * <username>xxx</username>
 * <password>密文密码</password>
 * </com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
 * <p>
 * 其中,password 里的用{}包裹的就是我们要用的,把大括号一起复制到下面代码里
 * <password>{AQAAABAAAAAgw5ss6376487dhjvchjvcjhdgkvjvkljlk}</password>
 * <p>
 * <p>
 * 访问和转储Jenkins凭证 https://blog.csdn.net/danpob13624/article/details/106778098
 * <p>
 * Jenkins如何存储凭证
 * 要访问和解密Jenkins凭证，您需要三个文件。
 * <p>
 * credentials.xml –保存加密的凭据
 * hudson.util.Secret –解密credentials.xml hudson.util.Secret条目，此文件本身已加密
 * master.key –解密hudson.util.Secret
 * 这三个文件都位于Jenkins主目录中：
 * <p>
 * $JENKINS_HOME/credentials.xml
 * $JENKINS_HOME/secrets/master.key
 * $JENKINS_HOME/secrets/hudson.util.Secret
 * 由于Jenkins是开源的，因此有人已经对加密和解密过程进行了反向工程。 如果您对这些细节感兴趣，请阅读这个迷人的博客 。
 * <p>
 * 秘密被以加密credentials.xml使用AES-128与hudson.util.Secret作为密钥，然后是base64编码。
 * hudson.util.Secret二进制文件使用master.key加密。
 * master.key以纯文本存储。
 * <p>
 * credentials.xml存储Global和System凭据。
 */
public class JenkinsEncodeTest {

    private static final String KEY_ALGORITHM = "AES";
    private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
    // secrets文件夹路径 服务器上在/root/.jenkins/secret，可以下载到本地
    private final String rootDir = "java-tools\\src\\main\\java\\jenkins";
    private static final byte[] MAGIC = "::::MAGIC::::".getBytes();
    // 密文密码
    private static final String data = "{AES-128加密，然后base64编码之后的密码}";

    public static void main(String[] args) {
        // 获取到的都是项目路径 获取当前工作路径 http://www.wld5.com/javajc/37294.html
        String currentPath = System.getProperty("user.dir");
        System.out.println("当前Java文件路径为：" + currentPath); // D:\work\gitee\文件-切片加密合并等\java-learn-demo

        // 使用File提供的函数获取当前路径 https://www.eolink.com/news/post/75639.html
        File directory = new File(".");//设定为当前文件夹
        try {
            /*
            File.getCanonicalPath()和File.getAbsolutePath()大约只是对于new File(".")和new File("..")两种路径有所区别。
            对于getCanonicalPath()函数，“."就表示当前的文件夹，而”..“则表示当前文件夹的上一级文件夹
            对于getAbsolutePath()函数，则不管”.”、“..”，返回当前的路径加上你在new File()时设定的路径
            至于getPath()函数，得到的只是你在new File()时设定的路径
            */
            System.out.println(directory.getCanonicalPath()); //获取标准的路径 D:\work\gitee\文件-切片加密合并等\java-learn-demo
            System.out.println(directory.getAbsolutePath()); //获取绝对路径 D:\work\gitee\文件-切片加密合并等\java-learn-demo\.
            System.out.println(directory.getPath()); // .
        } catch (Exception e) {
        }

        JenkinsEncodeTest test = new JenkinsEncodeTest();
        test.decrypt();
    }

    /**
     * 解密
     */
    public void decrypt() {

        byte[] payload;
        try {
            payload = Base64.getDecoder().decode(data.substring(1, data.length() - 1));
        } catch (IllegalArgumentException e) {
            return;
        }
        switch (payload[0]) {
            case 1:
                // For PAYLOAD_V1 we use this byte shifting model, V2 probably will need DataOutput
                int ivLength = ((payload[1] & 0xff) << 24)
                        | ((payload[2] & 0xff) << 16)
                        | ((payload[3] & 0xff) << 8)
                        | (payload[4] & 0xff);
                int dataLength = ((payload[5] & 0xff) << 24)
                        | ((payload[6] & 0xff) << 16)
                        | ((payload[7] & 0xff) << 8)
                        | (payload[8] & 0xff);
                if (payload.length != 1 + 8 + ivLength + dataLength) {
                    // not valid v1
                    return;
                }
                byte[] iv = Arrays.copyOfRange(payload, 9, 9 + ivLength);
                byte[] code = Arrays.copyOfRange(payload, 9 + ivLength, payload.length);
                String text;
                try {
                    text = new String(decrypt(iv).doFinal(code), UTF_8);
                    System.out.println("密码明文:" + text);
                } catch (GeneralSecurityException e) {
                    e.printStackTrace();
                    // it's v1 which cannot be historical, but not decrypting
                    return;
                }
//                return new Secret(text, iv);
            default:
                return;
        }

    }

    public Cipher decrypt(byte[] iv) {
        try {
            Cipher cipher = getCipher(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, getKey(), new IvParameterSpec(iv));
            return cipher;
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

    private synchronized SecretKey getKey() throws Exception {
        SecretKey secret = null;
        try {
            byte[] payload = load();
            if (payload == null) {
                payload = randomBytes(256);
                // store(payload);
            }
            // Due to the stupid US export restriction JDK only ships 128bit version.
            secret = new SecretKeySpec(payload, 0, 128 / 8, KEY_ALGORITHM);
        } catch (IOException e) {
            throw e;
        }
        return secret;
    }

    protected byte[] load() throws Exception {
        try {
            File f = new File(rootDir, "hudson.util.Secret");
            if (!f.exists()) {
                System.out.println("文件不存在：hudson.util.Secret");
                return null;
            }

            Cipher sym = getCipher("AES");
            sym.init(Cipher.DECRYPT_MODE, getMasterKey());
            try (InputStream fis = Files.newInputStream(f.toPath());
                 CipherInputStream cis = new CipherInputStream(fis, sym)) {
                byte[] bytes = IOUtils.toByteArray(cis);
                return verifyMagic(bytes);
            }
        } catch (Exception x) {
            if (x.getCause() instanceof BadPaddingException) {
                throw x; // broken somehow
            } else {
                throw x;
            }
        }
    }

    public static Cipher getCipher(String algorithm) throws GeneralSecurityException {
        return Cipher.getInstance(algorithm);
    }

    private SecretKey getMasterKey() throws Exception {
        File file = new File(rootDir, "master.key");
        if (!file.exists()) {
            System.out.println("文件不存在：master.key");
            return null;
        }
        SecretKey masterKey = toAes128Key(read(file).trim());

        return masterKey;
    }

    public String read(File file) throws Exception {
        StringWriter out = new StringWriter();
        PrintWriter w = new PrintWriter(out);
        BufferedReader in = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8);
        String line;
        while ((line = in.readLine()) != null)
            w.println(line);
        return out.toString();
    }

    public byte[] randomBytes(int size) {
        byte[] random = new byte[size];
        new SecureRandom().nextBytes(random);
        return random;
    }

    private byte[] verifyMagic(byte[] payload) {
        int payloadLen = payload.length - MAGIC.length;
        if (payloadLen < 0) return null;    // obviously broken

        for (int i = 0; i < MAGIC.length; i++) {
            if (payload[payloadLen + i] != MAGIC[i])
                return null;    // broken
        }
        byte[] truncated = new byte[payloadLen];
        System.arraycopy(payload, 0, truncated, 0, truncated.length);
        return truncated;
    }

    public static String toHexString(byte[] bytes) {
        int start = 0;
        int len = bytes.length;

        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < len; i++) {
            int b = bytes[start + i] & 0xFF;
            if (b < 16) buf.append('0');
            buf.append(Integer.toHexString(b));
        }
        return buf.toString();
    }

    public static SecretKey toAes128Key(String s) {
        try {
            // turn secretKey into 256 bit hash
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            digest.reset();
            digest.update(s.getBytes(StandardCharsets.UTF_8));

            // Due to the stupid US export restriction JDK only ships 128bit version.
            return new SecretKeySpec(digest.digest(), 0, 128 / 8, "AES");
        } catch (NoSuchAlgorithmException e) {
            throw new Error(e);
        }
    }

}
